Dart Zone

A Zone in Dart provides a way to override the default behavior of the language constructs. It's essentially an execution context that captures asynchronous operations. You can think of a zone as a sandbox in which code runs, where certain operations can be intercepted or replaced.

Error Handling and Asynchronous Operations: Zones are particularly useful for error handling and managing asynchronous operations. For example, a zone can capture uncaught errors, providing a way to handle them gracefully. It can also keep track of asynchronous tasks to know when they're all completed.

Storing and Retrieving Values: Zones can store key-value pairs, allowing data to be shared across the asynchronous flow of a program without having to explicitly pass them around. This is particularly useful for maintaining state across asynchronous callbacks that might otherwise be disconnected from each other.

基础概念

层级嵌套

Zone 可以嵌套,形成一个 Zone 层级。每个 Zone 可以有自己的属性和处理器。通过 Zone.current 可以访问当前 Zone。

root Zone

main 入口函数运行在默认的 Zone 当中(Zone.root)。

高级用法

Dart 的 Zone 类和其中的 registerCallback 方法为许多高级特性提供了基础,特别是在异步编程和资源管理方面。以下是一些在 Dart 和 Flutter 中可能基于 ZoneregisterCallback 实现的高级特性:

错误处理和诊断

Zone 允许自定义错误处理,可以捕获和处理在特定执行上下文中抛出的未捕获的错误。此外,可以通过 Zone 在注册回调时捕获注册时堆栈跟踪,以便于诊断。

性能分析和监控

通过 Zone,开发人员可以包装回调以测量执行时间,从而对代码性能进行分析。这对于找出性能瓶颈和优化应用程序非常有用。

异步任务的协调和控制

Zone 可用于协调和控制异步任务,例如并发控制、任务取消、任务优先级管理等。

资源管理和依赖注入

Zone 可以存储与特定执行上下文相关的资源(例如数据库连接、HTTP 客户端等)。这使得资源管理和依赖注入成为可能。

测试和模拟

在测试环境中,Zone 可用于模拟异步操作,如定时器、微任务等。这样可以在测试中更好地控制和模拟异步代码的行为。

Flutter 的异步绑定

Flutter 使用 Zone 来确保异步操作与正确的 widget 绑定和生命周期同步。例如,通过 Zone,Flutter 可以确保在 widget 被卸载后不会执行与之相关的异步回调。

示例

创建和运行 Zone

有两种方法:

  1. runZoned
  2. runZonedGuarded

创建 Zone 很简单,可以使用 runZoned 方法:

runZoned(() {
  // Zone 内的代码
}, onError: (error, stackTrace) {
  // 处理 Zone 内部的错误
});

异常捕获

以下是一个使用 Zone 捕获错误的示例:

runZoned(() {
  // 抛出一个异常
  throw 'Zone Error';
}, onError: (error, stackTrace) {
  print('Caught error: $error');
});

问题思考

rootZone 异步异常

在 Dart 的 rootZone(根区域)中发生异步异常时,该异常的处理方式与同步异常相同。如果未捕获,程序会打印错误堆栈并继续运行。

考虑代码:

void main() {
  Future.error(NullPointerException());
  print('Program continues running');
}

当在 rootZone(例如 main 函数中)发生异步异常时,如果没有专门的错误处理逻辑(例如通过 catchError 捕获异步错误),该异常会被传递到根区域的 handleUncaughtError 方法。

在根区域中,handleUncaughtError默认行为是打印未捕获的异步错误和堆栈跟踪,然后继续执行。这与未捕获的同步异常的处理方式相同。

程序会退出吗?

程序不会因此异常而退出。即使在 main 函数中发生了未捕获的异步异常,程序也会继续运行其他代码和其他异步任务。

Zone 类

代码位于:sdk/lib/async/zone.dart

handleUncaughtError

方法签名:

void handleUncaughtError(Object error, StackTrace stackTrace);

处理未捕获的异步错误。处理两类异步错误:

  1. 在异步回调中抛出的未捕获错误:例如,你安排了一个定时器,并在定时器的回调函数中抛出了一个错误,但没有捕获它。这种错误就会被这个方法处理。

  2. 通过 Future 和 Stream 传播的异步错误,但没有人注册错误处理器:许多异步类,如 Future 或 Stream,会将错误推送给它们的监听器。错误会这样传播,直到有监听器处理它(例如通过 Future.catchError)或没有监听器可用。在后者的情况下,Future 和 Stream 会调用 Zone 的 handleUncaughtError 方法。

errorZone

变量签名:

Zone get errorZone;

当发生未捕获的异步错误时,errorZone 就是负责处理这个错误的 Zone。

Zone 支持嵌套,当异步异常发生时,会由异步所在的 Zone 进行处理。并且在该总内消化,不会再向上传递。

fork

方法签名:

Zone fork({
	ZoneSpecification? specification, 
	Map<Object?, Object?>? zoneValues
});

fork 方法可以视为“分叉”或“创建分支”。当你在一个 Zone 内调用这个方法时,你实际上是在创建一个新的子 Zone,这个子 Zone 可以继承父 Zone 的某些行为,也可以覆盖它们。

参数

应用场景

场景 1:自定义错误处理

你可能想要在某个特定部分的代码中有不同的错误处理方式。通过使用 fork 创建一个新的 Zone,并提供自定义的错误处理函数,你可以实现这一目标。

var customZone = Zone.current.fork(
  specification: ZoneSpecification(
    handleUncaughtError: (self, parent, zone, error, stackTrace) {
      print('Custom error handling: $error');
    },
  ),
);

customZone.run(() {
  // 这里的代码将使用自定义的错误处理
});

场景 2:资源管理

如果你有一些与特定任务相关的资源(例如数据库连接),你可以使用 zoneValues 在 Zone 中存储这些资源。这样,在整个 Zone 的代码中,你都可以轻松访问这些资源。

var resourceZone = Zone.current.fork(
  zoneValues: {'dbConnection': DatabaseConnection()},
);

resourceZone.run(() {
  var dbConnection = Zone.current['dbConnection'];
  // 使用数据库连接
});

run

方法签名:

R run<R>(R action());

run 方法的主要作用是在当前 Zone 中执行一个给定的函数(称为“动作”)。当你在一个 Zone 中调用 run 方法并传递一个函数时,该函数就会在该 Zone 的上下文中执行。

action() 函数在 Zone.run 方法中是被同步执行的。当你调用 Zone.run(action) 时,action() 函数将在当前调用线程上立即执行,并且 Zone.run 方法将等待 action() 完成执行后才继续。

这意味着任何在 action() 函数中抛出的同步异常都不会被 Zone 的错误处理器捕获,因为它们发生在同步代码路径中。如果你想捕获这些同步异常,你可以使用 Zone.runGuarded 方法,或者在 action() 函数内部使用 try-catch 块。

Zone.current.run(() {
  print('This is executed synchronously.');
  // 任何在此处发生的同步异常将不会被 Zone 的错误处理器捕获
});

应用场景

场景 1:在自定义 Zone 中执行代码

假设你已经使用 fork 方法创建了一个自定义 Zone,现在你想在该 Zone 中执行某些代码。你可以使用 run 方法:

var customZone = Zone.current.fork(
  // 自定义 Zone 的行为
);

customZone.run(() {
  // 这里的代码将在 customZone 中执行
});

场景 2:执行带有资源的操作

参见 fork

场景 3:与错误处理结合

区别在于,通过 runGuarded 方法,将 action() 中的同步异常,也能捕获到,并统一放到 handleUncaughtError 中处理。

虽然 run 方法本身不会捕获同步异常,但你可以将其与 runGuarded 结合使用,以便在自定义 Zone 中捕获异常:

var errorHandlingZone = Zone.current.fork(
  specification: ZoneSpecification(
    handleUncaughtError: (self, parent, zone, error, stackTrace) {
      print('Caught error: $error');
    },
  ),
);

errorHandlingZone.runGuarded(() {
  // 这里的代码如果抛出错误,将被 handleUncaughtError 处理
});

runUnary 和 runBinary

run 的变体,分别处理 action() 带有一个和两个参数的场景:

R runUnary<R, T>(R action(T argument), T argument);

R runBinary<R, T1, T2>(
	R action(T1 argument1, T2 argument2), 
	T1 argument1, 
	T2 argument2);

runGuarded

参见 run。同时也存在多个变体:

void runGuarded(void action());

void runUnaryGuarded<T>(void action(T argument), T argument);

void runBinaryGuarded<T1, T2>(
	void action(T1 argument1, T2 argument2), 
	T1 argument1, 
	T2 argument2);

registerCallback

registerCallback 方法允许你在当前 Zone 中注册一个回调函数。注册回调的目的是让 Zone 能够在需要的时候记录一些信息,甚至可能包装回调,以便稍后在同一个 Zone 中运行。

函数签名:

ZoneCallback<R> registerCallback<R>(R callback());

参数:callback:你想要在当前 Zone 中注册的函数。

返回值:用来替代提供的回调的回调。通常,Zone 会简单地返回原始回调。也允许开发者对回调进行包装、定制。

什么是 Zone 回调?Zone 的回调是一个在特定 Zone 中注册并稍后在同一 Zone 中执行的函数。

为什么要注册回调?注册回调的目的是创建一个切面,让 Zone 有机会、有能力对所有异步操作进行管理。Zone 可以利用这一个切面,记录一些信息,甚至可以包装回调,以便稍后在同一个 Zone 中运行。

前面说道,Zone 能够对异步操作(比如 Future)进行管理。这是如何实现的呢?想必 Future 的实现源码中,必然与 Zone 之间有所关联。

registerCallback 便是实现这一关联的底层机制。比如,在 Dart Future 的实现源码中:

Future<T> whenComplete(dynamic action()) {
	_Future<T> result = new _Future<T>();
	if (!identical(result._zone, _rootZone)) {
	  action = result._zone.registerCallback<dynamic>(action);
	}
	_addListener(new _FutureListener<T, T>.whenComplete(result, action));
return result;
}

粗浅来看,在 Future 的代码实现中调用了 Zone 的 registerCallback。

代码位于:sdk/lib/async/future_impl.dart

应用场景

场景 1:记录堆栈跟踪

当异步操作报错的时候,报错堆栈指向的是运行报错的那一行。而更多的时候,我们希望知道这个异步是什么时候注册的,通过 registerCallback 便可以实现这一功能:

var stackTraceZone = Zone.current.fork(
  specification: ZoneSpecification(
    registerCallback: (self, parent, zone, callback) {
      var stackTrace = StackTrace.current;
      print('Callback registered with stack trace: $stackTrace');
      return parent.registerCallback(zone, callback);
    },
  ),
);

stackTraceZone.run(() {
  // 注册回调
});

在这段代码中,我们实际是实现了 registerCallback 回调。前面说道,Future 内部也调用 Zone 的 registerCallback,因此它每次调用,都会触发我们这里的调用。

而在这里的调用中,获取了当前的堆栈,这是注册时的

示例代码比较简单,仅仅是将堆栈打印出来。而实际上,可以将堆栈存起来了,等到异步报错的时候,在报错回调里进行查找,找出对应异步回调注册时的堆栈,一起进行上报。这样,在浏览异步报错时,不仅知道报错时的那一行,也知道是在那一行注册的,对于定位 Bug 来说,比较理想了。

自己保存堆栈,需要考虑的设计细节很多,比如一个异步操作如果执行成功,我们也要进行监听,以便删除它的堆栈,来保障不浪费内存。

场景 2:包装回调

你可能希望在回调执行之前或之后添加一些自定义逻辑。通过使用 registerCallback,你可以包装原始回调,并在执行回调之前或之后添加自定义代码。

相当于对异步操作进行切面编程,获得运行前和运行后两个切面:

var wrapZone = Zone.current.fork(
  specification: ZoneSpecification(
    registerCallback: (self, parent, zone, callback) {
      return () {
        print('Before callback execution');
        var result = callback();
        print('After callback execution');
        return result;
      };
    },
  ),
);

wrapZone.run(() {
  // 注册回调
});

这可以做异步消息的耗时统计。以便及时发现慢回调。慢回调会阻塞消息队列,导致 Flutter 的 UI 任务得不到响应,出现 UI 卡顿。

场景 3:从头实现一个定时器

假设你正在实现一个定时器库,并想让用户能够在定时器触发时提供一个回调。你可以这样做:

class MyTimer {
  final Duration duration;
  final ZoneCallback<void Function()> callback;

  MyTimer(this.duration, void Function() userCallback)
      : callback = Zone.current.registerCallback(userCallback);

  void start() {
    Timer(duration, () {
      // 在注册回调时的相同 Zone 中执行回调
      Zone.current.run(callback);
    });
  }
}

Zone 的异步调度接口

在 Zone 中,还有一些列异步调度接口:

其中,Timer 的构造函数即先走了 Zone 的 createTimer,再走 Timer 内部实现。这样实现了给 Zone 一个机会,对定时器进行处理:

factory Timer(Duration duration, void Function() callback) {
    if (Zone.current == Zone.root) {
      // No need to bind the callback. We know that the root's timer will
      // be invoked in the root zone.
      return Zone.current.createTimer(duration, callback);
    }
    return Zone.current.createTimer(
		        duration, 
		        Zone.current.bindCallbackGuarded(callback));
}

再看全局的调度微任务的实现 scheduleMicrotask,其内部也是通过 Zone 来转一道:

@pragma('vm:entry-point', 'call')
void scheduleMicrotask(void Function() callback) {
  _Zone currentZone = Zone._current;
  if (identical(_rootZone, currentZone)) {
    // No need to bind the callback. We know that the root's scheduleMicrotask
    // will be invoked in the root zone.
    _rootScheduleMicrotask(null, null, _rootZone, callback);
    return;
  }
  _ZoneFunction implementation = currentZone._scheduleMicrotask;
  if (identical(_rootZone, implementation.zone) &&
      _rootZone.inSameErrorZone(currentZone)) {
    _rootScheduleMicrotask(
        null, null, 
        currentZone, 
        currentZone.registerCallback(callback));
    return;
  }
  Zone.current.scheduleMicrotask(
	  Zone.current.bindCallbackGuarded(callback));
}

异步控制

Zone 提供了对异步任务的细粒度控制,允许开发人员实现并发控制、任务取消和任务优先级管理等功能。这些特性可以通过 Zone 的拦截机制和 Zone 局部值来实现,使得异步代码更容易管理和控制。无论是构建复杂的后端服务还是响应式的客户端应用程序,Zone 都可以作为强大的工具来提高代码的健壮性和可维护性。

并发控制

Zone 可以用于控制同时运行的异步任务数量。你可以通过拦截任务的调度来实现这一点,并使用信号量或其他并发控制机制来限制正在执行的任务数量。

class ConcurrencyControlZone {
  final int maxConcurrency;
  final _semaphore = … // 信号量或其他并发控制机制

  ZoneSpecification get specification => ZoneSpecification(
        registerCallback: (self, parent, zone, callback) {
          return () {
            if (_semaphore.acquire()) {
              try {
                return parent.registerCallback(zone, callback)();
              } finally {
                _semaphore.release();
              }
            }
          };
        },
      );

  // ... 创建并运行 Zone 的其他代码
}

任务取消

通过 Zone,你可以实现任务取消机制。你可以在 Zone 的上下文中存储一个“取消令牌”,并在启动任务时检查该令牌。

var cancelToken = CancelToken();
var cancelableZone = Zone.current.fork(
	zoneValues: {'cancelToken': cancelToken});

cancelableZone.run(() {
  if (Zone.current['cancelToken'].isCanceled) return;
  // 执行任务代码
});

// 取消任务
cancelToken.cancel();

任务优先级管理

你可以通过 Zone 将任务分配到具有不同优先级的队列中。通过拦截任务的调度并根据优先级将任务放入相应的队列,你可以控制任务的执行顺序。

class PriorityZone {
  final PriorityQueue _queue; // 优先队列

  ZoneSpecification get specification => ZoneSpecification(
        registerCallback: (self, parent, zone, callback) {
          return () {
            var priority = Zone.current['priority'];
            _queue.add(callback, priority: priority);
          };
        },
      );

  // ... 创建并运行 Zone 的其他代码
}

本文作者:Maeiee

本文链接:Dart Zone

版权声明:如无特别声明,本文即为原创文章,版权归 Maeiee 所有,未经允许不得转载!


喜欢我文章的朋友请随缘打赏,鼓励我创作更多更好的作品!